<?php

abstract class RestClient
{
    private $username;
    private $password;

    private $lastResponseCode = 0;

    public function SetAuthorizationData( $username, $password )
    {
        $this->username = $username;
        $this->password = $password;
    }

    protected function SendRequest( $url, $params, $method, $headers = null )
    {
        $query = '';

        if ( $method == RequestMethod::PUT )
        {
            // emulate PUT method by appending _method param to query
            $query = '_method=' . RequestMethod::PUT;
        }

        if ( is_array($params) )
        {
            foreach ( $params as $name => $value )
            {
                if (is_array($value) || is_object($value))
                {
                    $query .= "&" . http_build_query( array($name => $value) );
                }
                else
                {
                    if ( is_null($value) )
                    {
                        $value = '';
                    }
                    else if ( is_bool($value) )
                    {
                        $value = $value ? '1' : '0';
                    }

                    $query .= "&$name=". rawurlencode( $value );
                }
            }
            $query = ltrim($query, '&');
        }
        else if ( is_string($params) )
        {
            $query .= $params;
        }

        $ch = curl_init();

        if ( !empty($this->username) && !empty($this->password) )
        {
            curl_setopt( $ch, CURLOPT_USERPWD, "{$this->username}:{$this->password}" );
        }

        if ( !empty($headers) && is_array($headers) )
        {
            curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
        }

        if ( $method == RequestMethod::POST || $method == RequestMethod::PUT )
        {
            curl_setopt( $ch, CURLOPT_POST, TRUE );
            curl_setopt( $ch, CURLOPT_POSTFIELDS, $query );
        }
        else if ( $method == RequestMethod::GET )
        {
            if ( strpos($url, '?') === false )
            {
                $query = '?' . $query;
            }
            $url .= $query;
        }

        curl_setopt( $ch, CURLOPT_URL, $url );
        curl_setopt( $ch, CURLOPT_HEADER, true );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        $data = curl_exec( $ch );

        $result = self::ParseRequestResult($ch, $data);

        curl_close( $ch );

        $this->lastResponseCode = $result->code;

        return $result;
    }


    /**
     * Sends a post multipart/form-data request.
     * NOTE!!!!! This method strips the '@' symbol at the beginnings of string values (due to a curl quirk).
     *
     * This method should be used to submit a request with files.
     * To upload a file just use an instance of RestClientFileParam() as a parameter value.
     *
     * @param $url
     * @param array $params
     * @param array $headers
     *
     * @return RequestResult
     */
    public function SendMultipartPost( $url, array $params, $headers = null )
    {
        $formData = self::GetFormDataParams($params);

        $headers = array_merge(
            array_map('strval', $formData->GetHeaders(null)),
            !empty($headers) && is_array($headers)
                ? $headers
                : array()
        );
        $query = $formData->GetData();

        $ch = curl_init();

        if ( !empty($this->username) && !empty($this->password) )
        {
            curl_setopt( $ch, CURLOPT_USERPWD, "{$this->username}:{$this->password}" );
        }

        if ( !empty($headers) && is_array($headers) )
        {
            curl_setopt( $ch, CURLOPT_HTTPHEADER, $headers );
        }

        curl_setopt( $ch, CURLOPT_POST, TRUE );
        curl_setopt( $ch, CURLOPT_POSTFIELDS, $query );

        curl_setopt( $ch, CURLOPT_URL, $url );
        curl_setopt( $ch, CURLOPT_HEADER, true );
        curl_setopt( $ch, CURLOPT_RETURNTRANSFER, true );
        $data = curl_exec( $ch );

        $result = self::ParseRequestResult($ch, $data);

        curl_close( $ch );

        $this->lastResponseCode = $result->code;

        return $result;
    }

    protected static function ParseRequestResult($ch, $data)
    {
        $headerAndContents = preg_split("/(?:\r\n){2}/", $data, 2);
        $header = explode("\r\n", $headerAndContents[0]);

        while ( self::ParseHttpStatus( $header[0] ) == 100 )
        {
            $data = str_replace("HTTP/1.1 100 Continue\r\n\r\n", "", $data);
            $headerAndContents = preg_split("/(?:\r\n){2}/", $data, 2);
            $header = explode("\r\n", $headerAndContents[0]);
        }

        $header = implode($header, "\n");

        preg_match_all("/^(.+?):(.*)$/m", $header, $matches, PREG_SET_ORDER);
        $headers = array();
        foreach ( $matches as $match )
        {
            $headers[trim($match[1])] = trim($match[2]);
        }

        $contents = "";
        if (isset($headerAndContents[1]))
        {
            $contents = $headerAndContents[1];
        }

        $result = new RequestResult();
        $result->code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $result->data = $contents;
        $result->headers = $headers;

        return $result;
    }

    public function GetLastResponseCode()
    {
        return $this->lastResponseCode;
    }

    /**
     * Parses header and returns status as int.
     *
     * @param string $header
     * @return int
     */
    public static function ParseHttpStatus($header)
    {
        preg_match('|HTTP/\d\.\d\s+(\d+)\s+.*|',$header,$match);
        return ($match && is_numeric($match[1])) ? intval($match[1]) : null;
    }

    /**
     * Flattens parameters array to the form, understandable by PHP, i.e.
     * array(
     *   'param1' => array('key1' => 'value1', 'key2' => 'value2'),
     *   'param2' => array('value1', 'value2),
     *   'param3' => 'test'
     * )
     * ... converts into ...
     * array(
     *   'param1[key1]' => 'value1',
     *   'param1[key2]' => 'value2',
     *   'param2[0]' => 'value1',
     *   'param2[1]' => 'value2',
     *   'param3'    => 'test'
     * )
     *
     * NOTE! This function doesn't check if the keys are already flatten, so they
     * will replace existing items or be replaced according to their position.
     *
     * NOTE! Invalid keys, such as "some_already_flatten_key[0]", in inner arrays
     * will result in the invalid parameter names in the script accepting the query.
     *
     * @static
     *
     * @param array $params
     * @param string $parentName
     *
     * @return array of the depth equals to 1.
     */
    public static function FlattenParams(array $params, $parentName = null)
    {
        $result = array();
        foreach ($params as $name => $param)
        {
            $fullName = $parentName
                ? $parentName . '[' . $name . ']'
                : $name;

            if (is_array($param))
            {
                $result = array_merge($result, self::FlattenParams($param, $fullName));
            }
            else
            {
                $result[$fullName] = $param;
            }
        }

        return $result;
    }

    /**
     * Creates an IFormData object, that consists of the flattened passed parameters.
     * It builds FormDataUrlencodedParams if there is no parameters other than FormDataString objects
     * or ones that don't implement IFormData.
     * Otherwise, it creates a FormDataMultipart instance.
     *
     * Note! It skips null values, converts anything it encounters to strings (unless FormDataUrlencodedParams and FormDataMultipart are
     * implemented differently).
     *
     * @static
     *
     * @param array $params
     *
     * @return IFormData
     */
    public static function GetFormDataParams(array $params)
    {
        $className = 'FormDataUrlencodedParams';

        $params = self::FlattenParams($params);
        foreach ($params as $param)
        {
            if ($param instanceof IFormData && !($param instanceof FormDataString))
            {
                $className = 'FormDataMultipartParams';
                break;
            }
        }
        return new $className($params);
    }
}

?>
